Android程序设计探索:MVP与模块化
MVP与模块化,关于Android程序设计探索。
该篇是来自于奔ben苯笨的投稿,一个很有思想的老司机。
奔ben苯笨的博客地址:
http://www.jianshu.com/p/fb057953131e
0. 背景
最早接触到MVP这种设计模式,是在14年读《打造高质量Android应用:Android开发必知的50个诀窍》一书中了解到,而之后也逐步尝试去使用,至今体验下来,它不是一个可以完美到可以生搬硬套到各个场景的模式,正确地使用才能最好地发挥它的作用。
1. 作用简介
分层:将代码分层,抽取出数据、模型、界面。
复用:对V层或者P层接口的多种实现。
2. 作用-分层
我们大部分对MVP着迷的一个原因是早期写业务复杂的Activity时,代码量过于庞大,导致可读性很差。
而MVP通过3层的分离,有效地减少了Activity的代码量。
对于这个作用的理解上,个人认为,只有代码量比较大(大于1000行),并且Activity内各个功能模块比较耦合的时候,适用MVP模式。
3. 作用-复用
这是MVP的另一个非常优雅的使用场景。
当需要实现多个布局界面,但业务逻辑却不相同的场景时(即一个V层对应多个P层),MVP非常适用。
当然,多个布局架构不一致,但业务逻辑一致的情况(即一个P层对应多个V层),MVP也适用,不过至今我还遇到这种情况。
以下举个案例:
需求是实现多个以下的界面,布局架构一致,但数据内容、触发逻辑都不相同。
①. V层
package com.benhero.design.mvp.view;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import com.benhero.design.R;
import com.benhero.design.mvp.bean.MvpItem;
import com.benhero.design.mvp.presenter.MvpPresenterD;
import com.benhero.design.mvp.presenter.MvpContract;
import com.benhero.design.mvp.presenter.MvpPresenterB;
import com.benhero.design.mvp.presenter.MvpPresenterC;
import com.benhero.design.mvp.presenter.MvpPresenterA;
import java.util.ArrayList;
import java.util.List;
/**
* MVP
*
* @author benhero
*/
public class MvpActivity extends AppCompatActivity implements MvpContract.View, View.OnClickListener {
public static final String EXTRA_ENTER = "enter";
/**
* 1 : A
*/
public static final int ENTER_A = 1;
/**
* 2 : B
*/
public static final int ENTER_B = 2;
/**
* 3 : C
*/
public static final int ENTER_C = 3;
/**
* 4 : D
*/
public static final int ENTER_D = 4;
private MvpContract.Presenter mPresenter;
private TextView mUpgradeBtn;
private ListView mListView;
private List<MvpItem> mList = new ArrayList<>();
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_mvp_layout);
initView();
checkIntent();
mListView.setAdapter(new MVPAdapter());
}
private void initView() {
mUpgradeBtn = (TextView) findViewById(R.id.mvp_btn);
mUpgradeBtn.setOnClickListener(this);
mListView = (ListView) findViewById(R.id.mvp_listview);
}
private void checkIntent() {
Intent intent = getIntent();
if (intent != null) {
int enter = intent.getIntExtra(EXTRA_ENTER, 0);
if (enter == 0) {
errorEnter();
} else {
initData(enter);
}
} else {
errorEnter();
}
}
/*
* 状态错误
*/
private void errorEnter() {
Toast.makeText(this, "Error Intent", Toast.LENGTH_SHORT).show();
finish();
}
private void initData(int extra) {
switch (extra) {
case ENTER_A:
mPresenter = new MvpPresenterA(this);
break;
case ENTER_B:
mPresenter = new MvpPresenterB(this);
break;
case ENTER_C:
mPresenter = new MvpPresenterC(this);
break;
case ENTER_D:
mPresenter = new MvpPresenterD(this);
break;
default:
errorEnter();
break;
} if (mPresenter != null) {
mPresenter.initData();
}
}
protected void onResume() {
super.onResume();
if (mPresenter != null) {
mPresenter.onResume();
}
}
public void onClick(View v) {
if (v.equals(mUpgradeBtn)) {
Intent intent = new Intent(this, MvpResultActivity.class);
intent.putExtra(MvpResultActivity.EXTRA_ENTER,mPresenter != null ? mPresenter.getEnter() :
MvpResultActivity.ENTER_MAIN);
this.startActivity(intent);
}
}
public void initData(List<MvpItem> list) {
mList.clear();
mList.addAll(list);
}
public void setTitleText(int id) {
setTitle(getString(id));
}
public void setUpgradeBtnText(int id) {
mUpgradeBtn.setText(id);
}
public void setPresenter(MvpContract.Presenter presenter) {
mPresenter = presenter;
}
/**
* 列表适配器
*
* @author benhero
*/
private class MVPAdapter extends BaseAdapter {
public int getCount() {
return mList.size();
}
public Object getItem(int position) {
return mList.get(position);
}
public long getItemId(int position) {
return position;
}
public View getView(int position, View convertView, ViewGroup parent) {
MyViewHolder holder;
if (convertView == null) {
holder = new MyViewHolder();
convertView = LayoutInflater.from(MvpActivity.this).inflate(R.layout.mvp_list_item, parent, false);
holder.mIndex = (TextView) convertView.findViewById(R.id.mvp_index);
holder.mTitle = (TextView) convertView.findViewById(R.id.mvp_title);
holder.mDesc = (TextView) convertView.findViewById(R.id.mvp_desc);
holder.mDivider = convertView.findViewById(R.id.mvp_divider);
convertView.setTag(holder);
} else {
holder = (MyViewHolder) convertView.getTag();
}
MvpItem itemBean = mList.get(position);
holder.mIndex.setText(position + 1 + "");
holder.mTitle.setText(itemBean.getTitleId());
holder.mDesc.setText(itemBean.getDescId());
holder.mDivider.setVisibility(position == mList.size() - 1 ? View.GONE : View.VISIBLE);
return convertView;
}
/**
* ViewHolder
*/
class MyViewHolder {
TextView mIndex;
TextView mTitle;
TextView mDesc;
View mDivider;
}
}
}
以上就是我们对V层的处理,根据不同的intent数据,选择不同的MvpPresenter来处理不同的界面数据和交互逻辑。
②. P层
以下是其中某个P层的代码案例。
package com.benhero.design.mvp.presenter;
import com.benhero.design.R;
import com.benhero.design.mvp.bean.MvpItem;
import com.benhero.design.mvp.view.MvpResultActivity;
import java.util.ArrayList;
import java.util.List;
/**
* MvpPresenterA
*
* @author benhero
*/
public class MvpPresenterA implements MvpContract.Presenter {
private final MvpContract.View mView;
public MvpPresenterA(MvpContract.View view) {
mView = view;
}
public void start() {
}
public void initData() {
List<MvpItem> list = new ArrayList<>();
list.add(createFactor(R.string.mvp_a_factor_title_1, R.string.mvp_a_factor_desc_1));
list.add(createFactor(R.string.mvp_a_factor_title_2, R.string.mvp_a_factor_desc_2));
mView.initData(list);
mView.setTitleText(R.string.mvp_a_title);
mView.setUpgradeBtnText(R.string.mvp_a_upgrade_btn);
}
private MvpItem createFactor(int titleId, int descId) {
MvpItem item = new MvpItem();
item.setTitleId(titleId);
item.setDescId(descId);
return item;
}
public int getEnter() {
return MvpResultActivity.ENTER_A;
}
public void onResume() {
}
}
③. V层与P层接口
而对于V与P的接口类,是参考谷歌MVP架构开源项目中对于这方面的设计。
具体到本文的案例,接口类如下:
package com.benhero.design.mvp.presenter;
import com.benhero.design.mvp.base.BasePresenter;
import com.benhero.design.mvp.base.BaseView;
import com.benhero.design.mvp.bean.MvpItem;
import java.util.List;/**
* MVP接口
*
* @author benhero
*/
public interface MvpContract {
/**
* MVP逻辑控制接口
*/
interface Presenter extends BasePresenter {
void initData();
int getEnter();
void onResume();
}
/**
* MVP界面接口
*/
interface View extends BaseView<Presenter> {
void initData(List<MvpItem> list);
void setTitleText(int id);
void setUpgradeBtnText(int id);
}
}
4. 弊端
MVP最大的弊端,应该是可读性。
当M层和V层之间的互相调用过多时,在调试或者阅读代码时候,需要不停地在两边不停地跳转。而若不采用MVP,且代码排序良好,则可以自上而下顺畅地阅读。
而影响可读性的另一个重大因素是接口!当你在阅读V层时,遇到一个P的调用,点击跳转,则先跳转到接口类,再点击跳转到实现,实在繁琐(当然也可以通过快捷键直接跳实现的方法。
5. 建议
若M层或者V层不存在复用的可能性,则直接抛弃接口!
接口本身是规范类的行为,从而实现复用,多态。
对于某些业务的开发,根本不存在复用的可能性,可以大胆地抛弃之。
接口还有另一个作用就是约束访问者的访问范围,视情况再决定是否使用。
而对于复用的场景,接口肯定是必不可少的。
二. 模块化
我们开发过程中,经常存在这样的场景:Activity界面可以分成多个模块,且每个模块之间的交互不多。此时,我们就可以采用模块化的思路去解决Activity代码量过大的问题。
1. 思路
其实在实现这方面的需求,Google已经提供了解决方案:Fragment。一个Activity分切成多个Fragment,而且还可以针对不同屏幕来组合视图结构,相当好用。Fragment本身会处理好Activity相关的生命周期,非常棒。
注意:若一个Activity里只包裹着一个Fragment,并且没有别的视图,那么没什么意义!年少时做过不少这种傻事了我。这种场景不如直接一个Activity。
2. 新概念
这里,需要引入一个新的概念:ViewHolder(你也可以用Presenter或者Module等来命名它)。
作用:界面相关的业务逻辑的封装处理,轻量级。大概基础类如下,可以根据自己的需求进行调整。
package com.benhero.design.module.base;
import android.view.View;
/**
* ViewHolder基类
*
* @author benhero
*/
public class ViewHolder {
private View mContentView;
public ViewHolder() {
}
public ViewHolder(View contentView) {
mContentView = contentView;
}
public final void setContentView(View contentView) {
mContentView = contentView;
}
public View getContentView() {
return mContentView;
}
}
3. 案例
以下是一个视图模块化比较清晰的界面,图如下,图1抽屉上滑后变成图2的效果,使用的是BottomSheet组件。
接下来,将从Activity→Fragment→ViewHolder一层一层展示如何将相对复杂的Activity模块化。
1. Activity
package com.benhero.design.module.activity;
import android.os.Bundle;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import com.benhero.design.R;
/**
* 模块化Activity
*
* @author benhero
*/
public class ModuleActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_module);
BottomSheetBehavior<View> behavior = BottomSheetBehavior.from(findViewById(R.id.activity_main_bottom_sheet));
behavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
}
}
<?xml version="1.0" encoding="utf-8"?><RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#282828"
tools:context="com.benhero.design.module.activity.ModuleActivity">
<fragment
android:id="@+id/activity_module_bg_fragment"
android:name="com.benhero.design.module.bg.ModuleBgFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_module_bg"/>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false">
<RelativeLayout
android:id="@+id/activity_main_bottom_sheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/common_margin"
android:layout_marginRight="@dimen/common_margin"
android:clipChildren="false"
android:clipToPadding="false"
app:behavior_hideable="true"
app:behavior_peekHeight="@dimen/main_bottom_sheet_peek_height"
app:elevation="40dp"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<fragment
android:id="@+id/activity_main_bottom_sheet_fragment"
android:name="com.benhero.design.module.bottom.ModuleBottomFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_module_bottom"/>
</RelativeLayout>
</android.support.design.widget.CoordinatorLayout></RelativeLayout>
2. 底层
底层视图相对简单点,就是一个TextView,故没有继续拆分。
package com.benhero.design.module.bg;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.benhero.design.R;
/**
* 模块化背景Fragment
*/
public class ModuleBgFragment extends Fragment {
public ModuleBgFragment() {
}
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_module_bg, container, false);
}
}
3. 抽屉
①. Fragment
package com.benhero.design.module.bottom;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;import android.view.View;
import android.view.ViewGroup;
import com.benhero.design.R;
/**
* 模块化抽屉Fragment
*/
public class ModuleBottomFragment extends Fragment {
private ModuleBottomPeekViewHolder mPeekViewHolder;
private ModuleBottomListViewHolder mListViewHolder;
public ModuleBotto mFragment() {
}
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View layout = inflater.inflate(R.layout.fragment_module_bottom, container, false);
mPeekViewHolder = new ModuleBottomPeekViewHolder(this.getActivity(), layout.findViewById(R.id.bottom_peek_layout));
mListViewHolder = new ModuleBottomListViewHolder(layout.findViewById(R.id.fragment_bottom_sheet_list));
return layout;
}
}
这里我们通过ViewHolder将抽屉分成了2个模块。另外,布局xml如下。
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#6CD1CC"
android:orientation="vertical"
tools:context="com.benhero.design.module.bottom.ModuleBottomFragment">
<LinearLayout
android:id="@+id/bottom_peek_layout"
android:layout_width="match_parent"
android:layout_height="@dimen/main_bottom_sheet_peek_height"
android:orientation="horizontal">
<Button
android:id="@+id/bottom_peek_btn_1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="Btn 1"/>
<Button
android:id="@+id/bottom_peek_btn_2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="Btn 2"/>
</LinearLayout>
<include
layout="@layout/fragment_module_bottom_list"/>
</LinearLayout>
②. ViewHolder
package com.benhero.design.module.bottom;
import android.content.Context;
import android.view.View;
import android.widget.Toast;
import com.benhero.design.R;
import com.benhero.design.module.base.ViewHolder;
/**
* 抽屉顶部的ViewHolder
*
* @author benhero
*/
public class ModuleBottomPeekViewHolder extends ViewHolder implements View.OnClickListener {
private final Context mContext;
private View mBtn1;
private View mBtn2;
public ModuleBottomPeekViewHolder(Context context, View contentView) { super(contentView);
mContext = context;
initView();
}
private void initView() {
View contentView = getContentView();
mBtn1 = contentView.findViewById(R.id.bottom_peek_btn_1);
mBtn2 = contentView.findViewById(R.id.bottom_peek_btn_2);
mBtn1.setOnClickListener(this);
mBtn2.setOnClickListener(this);
}
public void onClick(View view) {
if (view.equals(mBtn1)) {
Toast.makeText(mContext, "Click Btn1", Toast.LENGTH_SHORT).show();
} else if (view.equals(mBtn2)) {
Toast.makeText(mContext, "Click Btn2", Toast.LENGTH_SHORT).show();
}
}
}
Github
本文案例已上传至Github - DesignExplore
总结
对于以上两种模式的使用场景,大概如下。
界面视图不可切割模块化,且视图、逻辑都不存在复用的可能:使用MVP,且无需抽接口
界面视图或逻辑存在复用的情况:使用MVP,并抽接口
界面视图可模块化,模块间较少关联:使用视图模块化的方式:Activity→Fragment→ViewHolder
若界面非常复杂,可以考虑两种方式同时使用
对于模块化的方案,不同模块间的通讯可以采用接口让上层去中转。更简单的是使用EventBus。
对于程序设计,每个人的理解可能都不一样,但我们的目标都是一致的,都是想让程序的可读性、逻辑性、拓展性等各方面都达到比较好的效果。
博客链接地址:
奔ben苯笨的博客地址:
http://www.jianshu.com/p/fb057953131e